using System;
using System.Threading;
using System.Collections;
using System.Text;
using System.Net;
using System.Net.Sockets;
using XYThreadPoolLib;
using System.Diagnostics;
using Magnolia;

namespace CommLayer
{
    class TcpIpClient
    {
        
        protected const int m_nPause = 50;
        protected String m_sRemoteAddress = "";
        protected int m_nRemotePort = 0;
        protected int m_nMaxDataSize = 4 * 1024 * 1024;
        protected int m_nReadTimeout = 30;
        protected InputProcessor m_inputProcessor;
        protected Transmission m_trn = null;
        protected IPEndPoint m_ipEndPoint;
        XYThreadPool m_threadPool = new XYThreadPool();
        public ConnectionFilterDelegate m_delegateConnectionFilter = null;
        public ExceptionHandlerDelegate m_delegateExceptionHandler = null;
        protected bool m_bListenOnLocalEndpoint = true;
        protected bool m_bConnectionEstablished = false;
        protected bool m_bConnecting = false;


		/// <summary>
		/// Constructor
		/// </summary>
		/// <param name="sRemoteAddress"></param>
		/// <param name="nRemotePort"></param>
        public TcpIpClient(String sRemoteAddress, int nRemotePort, ConnectionFilterDelegate delegateConnectionFilter,
                        StringInputHandlerDelegate delegateStringInputHandler,
                        ExceptionHandlerDelegate delegateExceptionHandler)
		{
            m_delegateConnectionFilter = delegateConnectionFilter;
            m_delegateExceptionHandler = delegateExceptionHandler; 
            
            if (sRemoteAddress != null) 
                m_sRemoteAddress = sRemoteAddress;
			if(nRemotePort>0) 
                m_nRemotePort = nRemotePort;

            //threadpool have more than 1 min threads as the first thread (LoopProcessInput) really never returns. the code of the 
            // threadpool dequeues it, but the thread number is still 1, CAUSING NEW  WORKITEMS TO BE EXECUTED ONLY WHEN A SECOND THREAD EXECS
            m_threadPool.StartThreadPool(6, 12);
            m_bConnectionEstablished = false;
            m_bConnecting = false;

            m_threadPool.SetThreadErrorHandler(new XYThreadPoolLib.ThreadErrorHandlerDelegate(ThreadErrorHandler));


    	}

        /// <summary>
        /// ThreadErrorHandler - report back to the excpetion handler
        /// </summary>
        /// <param name="oWorkItem"></param>
        /// <param name="oError"></param>
        protected void ThreadErrorHandler(ThreadPoolWorkItem oWorkItem, Exception oError)
        {
            // create a new exception with new info
            string error = "exception in thread " + oWorkItem.m_sName + " of type " + oError.GetType().ToString() + ": " + oError.Message;
            Exception ex1 = new Exception(error);
            m_delegateExceptionHandler(ex1);


        }

        /// <summary>
        /// Destructor - reset
        /// </summary>
        ~TcpIpClient()
		{
			Reset();
		}

        public bool ListenOnLocalEndpoint
        {
            get
            {
                return m_bListenOnLocalEndpoint;
            }
            set
            {
                m_bListenOnLocalEndpoint = value;
            }
        }

        /// <summary>
        /// Input processor reference
        /// </summary>
        public InputProcessor InputProcessor
        {

            get
            {
                return m_inputProcessor;
            }
            set
            {
                m_inputProcessor = value;
            }
        }

        /// <summary>
        /// Thread pool
        /// </summary>
        public XYThreadPool ThreadPool
        {
            get
            {
                return m_threadPool;
            }
        }

        /// <summary>
        /// in midst of Connect
        /// </summary>
        public bool Connecting
        {
            get
            {
                return m_bConnecting;
            }
        }

        /// <summary>
        /// Last exception that occured
        /// </summary>
        /// <returns></returns>
		public Exception GetLastException() 
		{
			Monitor.Enter(this);
			Exception exp = m_inputProcessor.m_exception;
			Monitor.Exit(this);
			return exp;
		}

        /// <summary>
        /// sets max data size 
        /// </summary>
        /// <param name="nMaxDataSize"></param>
		public void SetMaxDataSize(int nMaxDataSize)
		{
			if(nMaxDataSize>=1024) 
                m_nMaxDataSize = nMaxDataSize;
		}

        /// <summary>
        /// Get allocated socket
        /// </summary>
        /// <returns></returns>
		protected Socket GetSocket()
		{
			Monitor.Enter(this);
			Socket sock = m_trn.m_socket;
			Monitor.Exit(this);
			return sock;
		}

        /// <summary>
        /// Is the client connected
        /// </summary>
        /// <returns></returns>
        public bool IsConnected
        {
            get
            {
                bool bConnected = (m_bConnectionEstablished && m_trn != null && m_trn.m_socket != null && m_trn.m_socket.Connected); //Josh Changed 8/13/2007 for second check of .Connected
                return bConnected;
            }

        }

      
        /// <summary>
        /// Connects
        /// </summary>
        /// <returns></returns>
		public virtual bool Connect()
		{
            
			try
            {
				Monitor.Enter(this);
                // if two connects are entered one ofater the other, exit
                if (m_bConnectionEstablished)
                    return true;
                m_bConnecting = true;
                m_bConnectionEstablished = false;
				XYNetCommon.SetSocketPermission();

 			
				// trn will hold the socket
                m_trn = new Transmission(new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp));
                //
				//Reset();
                // calc the ip end point
				try
				{
                    m_ipEndPoint = (m_sRemoteAddress == "") ? (new IPEndPoint(Dns.GetHostByName(Dns.GetHostName()).AddressList[0], m_nRemotePort)) : (new IPEndPoint(IPAddress.Parse(m_sRemoteAddress), m_nRemotePort));
				}
				catch(Exception) {}

                if (m_ipEndPoint == null)
				{
                    m_ipEndPoint = new IPEndPoint(Dns.GetHostByName(m_sRemoteAddress).AddressList[0], m_nRemotePort);
				}
                
                m_trn.m_socket.Connect(m_ipEndPoint);

                // set keep alive on this connection. Note: that might be needed only from the servr
                XYNetCommon.SetKeepAlive(1, m_trn.m_socket);

                // make it as urgent as possible
                m_trn.m_socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
                m_trn.m_socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.BsdUrgent, true);
                m_trn.m_socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.Expedited, true);
                
                // and start the thread for receiving input
                m_inputProcessor.Start(m_trn, m_ipEndPoint);

                m_bConnectionEstablished = true;

                if (m_delegateConnectionFilter != null)
                {
                    // notifying connection through the connection filter call backk
                    m_threadPool.InsertWorkItem("Connection established", new ConnectionFilterDelegate(m_delegateConnectionFilter), new Object[3] { m_ipEndPoint.Address.ToString(), m_ipEndPoint.Port, m_trn.m_socket }, false);
                }

 				return true;
			}
			catch(Exception oBug)
			{
                // trace from here too
                Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceError, DateTime.Now.ToLongTimeString() + "-> Client Connect exception: " + oBug.Message);

                // report
                HandleException(oBug, m_trn);
                
				return false;
			}
			
            finally {
                m_bConnecting = false;
                Monitor.Exit(this);
                 }
		}

        /// <summary>
        /// Sends a bit stream
        /// </summary>
        /// <param name="pData"></param>
        /// <returns></returns>
		protected bool SendRawData(Byte[] pData)
		{
           
			try
			{
				Monitor.Enter(this);
                
                XYNetCommon.SendData(pData, m_trn.m_socket, m_nPause);

                
			}
			catch(Exception oBug)
			{
                // trace from here too
                Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceError, DateTime.Now.ToLongTimeString() + "-> Client SendRawData exception: " + oBug.Message);

				// report exception
                HandleException(oBug, m_trn);
                
				return false;
			}
            finally { 
                Monitor.Exit(this);
                 }

            return true;
		}

        /// <summary>
        /// send data preceeded by a length header
        /// </summary>
        /// <param name="pData"></param>
        /// <returns></returns>
		public bool SendBinaryData(Byte[] pData)
		{
			Byte[] pData2 = new Byte[pData.Length+4];
			pData2[0] = (Byte)(1+(pData.Length/16777216)*16);
			pData2[1] = (Byte)(pData.Length%256);
			pData2[2] = (Byte)((pData.Length%65536)/256);
			pData2[3] = (Byte)((pData.Length/65536)%256);
			pData.CopyTo(pData2,4);
			return SendRawData(pData2);
		}

        /// <summary>
        /// send string data preceeded by a length header
        /// </summary>
        /// <param name="sData"></param>
        /// <returns></returns>
		public virtual bool SendStringData(String sData)
		{
			Byte[] pData = new Byte[sData.Length*2+4];
			pData[0] = (Byte)(((2*sData.Length)/16777216)*16);
			pData[1] = (Byte)((2*sData.Length)%256);
			pData[2] = (Byte)(((2*sData.Length)%65536)/256);
			pData[3] = (Byte)(((2*sData.Length)/65536)%256);
			XYNetCommon.StringToBinary(sData).CopyTo(pData, 4);
			return SendRawData(pData);
		}

		/// <summary>
		/// sets maximum time out in ms. max: 1200. min: 5
		/// </summary>
		/// <param name="nReadTimeout"></param>
        public void SetReadTimeout(int nReadTimeout)
		{
			Monitor.Enter(this);
			if(nReadTimeout>=5&&nReadTimeout<=1200) 
                m_nReadTimeout = nReadTimeout;
			Monitor.Exit(this);
		}

        /// <summary>
        /// closes the connection
        /// </summary>
		public void Reset()
		{
			try
			{
                // disconnect from threads
				if(m_trn.m_socket!=null)
				{
					//m_trn.m_socket.Send(new Byte[4]{255, 0, 0, 0}, SocketFlags.None);
					m_trn.m_socket.Shutdown(SocketShutdown.Both);
					m_trn.m_socket.Close();
				}
                
			}
            catch (Exception oBug1)
            {
                m_delegateExceptionHandler(oBug1);
            }
            finally 
			{
                m_bConnectionEstablished = false;
                m_bConnecting = false;
				m_trn.m_socket = null;
			}
		}

        /// <summary>
        /// Handle client exception
        /// </summary>
        public void HandleException(Exception oBug, Transmission trn)
        {
            // reprot excpetion
            m_delegateExceptionHandler(oBug);
            // and reset the whole connection
            try
            {
                Reset();
            }
            catch (Exception oBug1)
            {
                m_delegateExceptionHandler(oBug1);
            }

        }

        /// <summary>
        /// try connect for timeout
        /// </summary>
        /// <param name="timeOutMs"></param>
        public void ConnectAsync()
        {
            try
            {
                DateTime timeNow = DateTime.Now;

                // try to connect
                Connect();

                // if not connected, close
                if (!IsConnected)
                {
                    TimeSpan diff = timeNow - DateTime.Now;
                    Exception oNoConnect = new Exception("connection timeout: cannot connect after " + diff.ToString());
                    
                    // trace from here too
                    Trace.WriteLineIf(AppContext.AppTraceSwitch.TraceError, DateTime.Now.ToLongTimeString() + "-> ConnectAsync exception: " + oNoConnect.Message);

                    HandleException(oNoConnect,m_trn);
     
                }
            }
            catch (Exception ex)
            {
                HandleException(ex, m_trn);
            }
        }
    }
}